package tech.yunqian.slim.project.service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import com.alibaba.fastjson.JSONObject;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import tech.yunqian.slim.project.domain.Terminal;
import tech.yunqian.slim.project.domain.ThingsBoardCredentials;
import tech.yunqian.slim.project.enums.ComparisonOperator;
import tech.yunqian.slim.project.enums.DataType;
import tech.yunqian.slim.project.domain.Device;
import tech.yunqian.slim.project.domain.Entity;
import tech.yunqian.slim.project.domain.Event;
import tech.yunqian.slim.project.domain.EventDetail;
import tech.yunqian.slim.project.domain.MonitorCondition;
import tech.yunqian.slim.project.domain.MonitorRule;
import tech.yunqian.slim.project.domain.Telemetry;
import tech.yunqian.slim.project.mapper.EntityMapper;
import tech.yunqian.slim.project.mapper.EventDetailMapper;
import tech.yunqian.slim.project.mapper.EventMapper;
import tech.yunqian.slim.project.mapper.MonitorConditionMapper;
import tech.yunqian.slim.project.mapper.MonitorRuleMapper;
import tech.yunqian.slim.project.mapper.ScenarioRuleMapper;
import tech.yunqian.slim.project.mapper.AlarmRuleMapper;
import tech.yunqian.slim.project.mapper.TelemetryMapper;
import tech.yunqian.slim.project.mapper.ThingsBoardCredentialsMapper;

@Slf4j
@Service
public class DeviceService {
    @Value("${tgk.padding:false}")
    private Boolean padding;

    @Autowired
    private ThingsBoardService thingsBoardService;

    @Autowired
    private MonitorTrackerService monitorTrackerService;

    @Autowired
    private EntityMapper entityMapper;

    @Autowired
    private TelemetryMapper telemetryMapper;

    @Autowired
    private EventMapper eventMapper;

    @Autowired
    private EventDetailMapper eventDetailMapper;

    @Autowired
    private AlarmRuleMapper alarmRuleMapper;

    @Autowired
    private ScenarioRuleMapper scenarioRuleMapper;

    @Autowired
    private MonitorRuleMapper monitorRuleMapper;

    @Autowired
    private MonitorConditionMapper monitorConditionMapper;

    @Autowired
    private ThingsBoardCredentialsMapper thingsBoardCredentialsMapper;

    public Device lookupByTerminalTbDeviceName(String name) {
        Device device = null;
        List<Entity> siblings = entityMapper.lookupSiblingsByTbDeviceName(name);
        log.debug("siblings: {}", siblings);
        if (siblings.isEmpty()) {
            return null;
        }
        try {
            List<Terminal> terminals = new ArrayList<>();
            for (Entity sibling : siblings) {
                if (device == null) {
                    device = new Device();
                    device.setId(sibling.getId());
                    device.setName(sibling.getName());
                    device.setProjectId(sibling.getProjectId());
                    device.setProjectName(sibling.getProjectName());
                    device.setProductId(sibling.getProductId());
                    device.setTbDeviceId(sibling.getDeviceTbDeviceId());
                    device.setTbDeviceName(sibling.getDeviceTbDeviceName());
                    device.setCompanyId(sibling.getCompanyId());
                }
                if (sibling.getActive()) {
                    device.setActiveNumber(sibling.getTerminalNumber());
                }
                Terminal terminal = new Terminal();
                terminal.setId(sibling.getTerminalId());
                terminal.setTerminalNumber(sibling.getTerminalNumber());
                terminal.setTbDeviceId(sibling.getTerminalTbDeviceId());
                terminal.setTbDeviceName(sibling.getTerminalTbDeviceName());
                terminal.setConvertor(sibling.getConvertor());
                terminals.add(terminal);
            }
            device.setTerminals(terminals);
        } catch (Exception e) {
            log.info(e.getMessage());
            StackTraceElement[] traces = e.getStackTrace();
            for (StackTraceElement trace : traces) {
                log.debug(trace.toString());
            }
            return null;
        }
        device.setTelemetries(telemetryMapper.getProductTelemetries(device.getProductId()));
        Long companyId = device.getCompanyId();
        ThingsBoardCredentials credentials = thingsBoardCredentialsMapper.lookupThingsBoardCredentials(companyId);
        device.setCredentials(credentials);
        device.setTelemetries(telemetryMapper.getProductTelemetries(device.getProductId()));
        device.setEvents(eventMapper.getProductEvents(device.getProductId()));
        return device;
    }

    public JSONObject getLatestTimeSeries(Device device) {
        JSONObject timeSeries = new JSONObject();
        ThingsBoardCredentials credentials = device.getCredentials();
        for (Terminal terminal : device.getTerminals()) {
            String tbDeviceId = terminal.getTbDeviceId();
            JSONObject entries = thingsBoardService.getLatestTimeSeries(credentials, tbDeviceId);
            log.info("\n>>> raw: {}\n", entries);
            String convertor = terminal.getConvertor();
            if (convertor != null && !convertor.isBlank()) {
                try {
                    convertor = new String(Base64.getDecoder().decode(convertor));
                    log.info("\n>>> convertor: {}\n", convertor);
                    convertor = "var output = {}; try {" + convertor + "} catch (e) {} output = JSON.stringify(output);";
                    ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
                    ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("javascript");
                    scriptEngine.put("input", entries);
                    scriptEngine.eval(convertor);
                    log.info("\n>>> input: {}\n", scriptEngine.get("input"));
                    log.info("\n>>> output: {}\n", scriptEngine.get("output"));
                    String output = scriptEngine.get("output").toString();
                    JSONObject converted = JSONObject.parseObject(output);
                    entries.putAll(converted);
                    log.info("\n>>> merged: {}\n", entries);
                } catch (Exception e) {
                    log.info(e.getMessage());
                    StackTraceElement[] traces = e.getStackTrace();
                    for (StackTraceElement trace : traces) {
                        log.debug(trace.toString());
                    }
                }
            }
            Integer terminalNumber = terminal.getTerminalNumber();
            Iterator<String> iterator = entries.keySet().iterator();
            while (iterator.hasNext()) {
                String key = iterator.next();
                for (Telemetry telemetry : device.getTelemetries()) {
                    if (telemetry.getTerminalNumber() == null || telemetry.getTerminalIdentifier() == null) {
                        continue;
                    }
                    if (telemetry.getTerminalNumber().equals(terminalNumber) && telemetry.getTerminalIdentifier().equals(key)) {
                        timeSeries.put(telemetry.getUserIdentifier(), entries.get(key));
                    }
                }
            }
        }
        log.debug("timeseries: {}", timeSeries);
        return timeSeries;
    }

    private Map<String, Object> buildVariables(Device device, JSONObject raw) {
        Map<String, Object> variables = new HashMap<>();
        Iterator<Telemetry> iterator = device.getTelemetries().iterator();
        while (iterator.hasNext()) {
            Telemetry telemetry = iterator.next();
            if (!telemetry.getIsComputed()) {
                String key = telemetry.getUserIdentifier();
                JSONObject latest = raw.getJSONObject(key);
                if (latest == null) {
                    continue;
                }
                variables.put(key, latest.get("value"));
            }
        }
        return variables;
    }

    public JSONObject cookTelemetries(Device device, JSONObject raw) {
        JSONObject cooked = new JSONObject();
        Map<String, Object> variables = buildVariables(device, raw);
        log.debug("variables: {}", variables);
        Iterator<Telemetry> iterator = device.getTelemetries().iterator();
        while (iterator.hasNext()) {
            Telemetry telemetry = iterator.next();
            String key = telemetry.getUserIdentifier();
            if (!telemetry.getIsComputed()) {
                if (telemetry.getTerminalNumber().equals(device.getActiveNumber()) || padding) {
                    cooked.put(key, variables.get(key));
                }
            } else {
                try {
                    ExpressionBuilder expressionBuilder = new ExpressionBuilder(telemetry.getFormula());
                    expressionBuilder.variables(variables.keySet());
                    Expression expression = expressionBuilder.build();
                    Iterator<String> variablesIterator = variables.keySet().iterator();
                    Integer fractionLength = 0;
                    while (variablesIterator.hasNext()) {
                        String name = variablesIterator.next();
                        try {
                            String valueString = variables.get(name).toString();
                            Integer pointIndex = valueString.indexOf(".");
                            if (pointIndex > 0) {
                                Integer valueFractionLength = valueString.length() - pointIndex - 1;
                                if (valueFractionLength > fractionLength) {
                                    fractionLength = valueFractionLength;
                                }
                            }
                            expression.setVariable(name, Double.parseDouble(valueString));
                        } catch (Exception e) {
                            log.info(e.getMessage());
                            StackTraceElement[] traces = e.getStackTrace();
                            for (StackTraceElement trace : traces) {
                                log.debug(trace.toString());
                            }
                        }
                    }
                    Double rawComputed = expression.evaluate();
                    BigDecimal bComputed= new BigDecimal(rawComputed);
                    Double roundedComputed = bComputed.setScale(fractionLength, RoundingMode.HALF_EVEN).doubleValue();
                    cooked.put(key, roundedComputed);
                } catch (Exception e) {
                    log.info(e.getMessage());
                    StackTraceElement[] traces = e.getStackTrace();
                    for (StackTraceElement trace : traces) {
                        log.debug(trace.toString());
                    }
                }
            }
        }
        String activeKey = "__active__" + device.getActiveNumber();
        Boolean active = raw.getBoolean(activeKey);
        if (active != null) {
            cooked.put(activeKey, active);
        }
        return cooked;
    }

    public Boolean deliverTelemetries(Device device, JSONObject telemetries) {
        Boolean done = false;
        ThingsBoardCredentials credentials = device.getCredentials();
        String tbDeviceId = device.getTbDeviceId();
        done = thingsBoardService.putTelemetries(credentials, tbDeviceId, telemetries);
        return done;
    }

    public JSONObject probeTelemetryTriggers(Device device, JSONObject raw) {
        if (device.getProjectId() == null) {
            return null;
        }
        Map<Integer, Telemetry> telemetryMap = new HashMap<>();
        for (Telemetry telemetry : device.getTelemetries()) {
            telemetryMap.put(telemetry.getId(), telemetry);
        }
        if (telemetryMap.isEmpty()) {
            return null;
        }
        List<MonitorCondition> monitorConditions = monitorConditionMapper.indexByTelemetryIds(new ArrayList<>(telemetryMap.keySet()));
        log.debug("conditions: {}", monitorConditions);
        for (MonitorCondition monitorCondition : monitorConditions) {
            Integer id = monitorCondition.getTelemetryId();
            if (!telemetryMap.keySet().contains(id)) {
                continue;
            }
            Telemetry telemetry = telemetryMap.get(id);
            String identifier = telemetry.getUserIdentifier();
            String value = raw.getString(identifier);
            if (value == null) {
                continue;
            }
            Map<String, Object> filters = new HashMap<>();
            filters.put("id", monitorCondition.getRuleId());
            MonitorRule monitorRule = monitorRuleMapper.retrieve(filters);
            filters.clear();
            filters.put("project_id", device.getProjectId());
            filters.put("monitor_rule_id", monitorCondition.getRuleId());
            monitorRule.setAlarmRules(alarmRuleMapper.lookup(filters));
            monitorRule.setScenarioRules(scenarioRuleMapper.lookup(filters));
            if (!monitorRule.isActive()) {
                continue;
            }
            String threshold = monitorCondition.getThreshold();
            ComparisonOperator comparisonOperator = ComparisonOperator.valueOfCode(monitorCondition.getComparisonOperator());
            DataType dataType = DataType.valueOfLabel(telemetry.getDataType());
            String step = telemetry.getStep();
            Boolean matched = ComparisonService.evaluate(value, threshold, step, dataType, comparisonOperator);
            log.info("\n++++++++++\n{}\n{}\n{}\n{}\n{}\n++++++++++\n", monitorCondition, telemetry, comparisonOperator, value, matched);
            Map<String, Object> info = new HashMap<>();
            info.put("device", device);
            info.put("monitorRule", monitorRule);
            info.put("monitorCondition", monitorCondition);
            info.put("value", value);
            info.put("timestamp", new Date());
            if (matched) {
                monitorTrackerService.raise(info);
            } else {
                monitorTrackerService.drop(info);
            }
        }
        return null;
    }

    public Boolean deliverEvents(Device device, JSONObject data) {
        Boolean done = false;
        Set<String> identifiers = data.keySet();
        Iterator<Event> iterator = device.getEvents().iterator();
        while (iterator.hasNext()) {
            Event event = iterator.next();
            String terminalIdentifier = event.getTerminalIdentifier();
            if (event.getTerminalNumber().equals(device.getActiveNumber())) {
                Integer terminalId = 0;
                for (Terminal terminal : device.getTerminals()) {
                    if (terminal.getTerminalNumber().equals(device.getActiveNumber())) {
                        terminalId = terminal.getId();
                    }
                }
                if (identifiers.contains(terminalIdentifier)) {
                    JSONObject params = data.getJSONObject(terminalIdentifier);
                    EventDetail eventDetail = new EventDetail();
                    eventDetail.setEventId(event.getId());
                    eventDetail.setTerminalId(terminalId);
                    eventDetail.setParam(params.toJSONString());
                    eventDetail.setCompanyId(device.getCompanyId());
                    eventDetail.setTimestamp(new Date());
                    eventDetailMapper.create(eventDetail);
                    log.info("event: {} {}", event, eventDetail);
                }
            }
        }
        return done;
    }

    public JSONObject probeEventTriggers(Device device, JSONObject raw) {
        if (device.getProjectId() == null) {
            return null;
        }
        Map<Integer, Event> eventMap = new HashMap<>();
        for (Event event : device.getEvents()) {
            log.info("{}", event);
            if (!device.getActiveNumber().equals(event.getTerminalNumber())) {
                continue;
            }
            eventMap.put(event.getId(), event);
        }
        log.info("{}", eventMap);
        if (eventMap.isEmpty()) {
            return null;
        }
        List<MonitorCondition> monitorConditions = monitorConditionMapper.indexByEventIds(new ArrayList<>(eventMap.keySet()));
        log.debug("conditions: {}", monitorConditions);
        for (MonitorCondition monitorCondition : monitorConditions) {
            Integer id = monitorCondition.getEventId();
            if (!eventMap.keySet().contains(id)) {
                continue;
            }
            Event event = eventMap.get(id);
            Map<String, Object> filters = new HashMap<>();
            filters.put("id", monitorCondition.getRuleId());
            MonitorRule monitorRule = monitorRuleMapper.retrieve(filters);
            filters.clear();
            filters.put("project_id", device.getProjectId());
            filters.put("monitor_rule_id", monitorCondition.getRuleId());
            monitorRule.setAlarmRules(alarmRuleMapper.lookup(filters));
            monitorRule.setScenarioRules(scenarioRuleMapper.lookup(filters));
            if (!monitorRule.isActive()) {
                continue;
            }
            log.info("\n++++++++++\n{}\n{}\n{}\n++++++++++\n", monitorCondition, event, raw);
            Map<String, Object> info = new HashMap<>();
            info.put("device", device);
            info.put("monitorRule", monitorRule);
            info.put("monitorCondition", monitorCondition);
            info.put("params", raw.getJSONObject(event.getTerminalIdentifier()).toJSONString());
            info.put("timestamp", new Date());
            monitorTrackerService.raise(info);
            monitorTrackerService.drop(info);
        }
        return null;
    }
}
